今天我要讓你能抓到一點寫程式的感覺,所以我們會一直不斷地修改程式碼,這麼做可以讓你對程式碼的操作更熟悉。
先從最簡單的功能開始作,今天的目標是讓卡米狗能針對關鍵字回應訊息。
在加程式碼之前,我們先整理一下目前的程式
def webhook
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '好哦~好哦~'
}
# 傳送訊息
response = client.reply_message(reply_token, message)
# 回應 200
head :ok
end
目前程式碼是這樣,我覺得有點太長了,我們要讓他更好閱讀。首先要制定一個目標。
def webhook
# 核心程式
reply_message = reply(received_message)
# 回覆訊息
reply_to_line(message)
# 回應 200
head :ok
end
只保留最重要的,當卡米狗看到 received_message
時,要回應 reply_message
,剩下的東西都放到別處。我們先假設卡米狗只會對純文字有反應,並且只會回應純文字。
# Line Bot API 物件初始化
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
我們把這段程式搬出去,變成這樣:
# Line Bot API 物件初始化
def line
client = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
定義一個方法叫作 line
,他會回傳一個 client。這裡可以省略區域變數 client 不寫。
# Line Bot API 物件初始化
def line
Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
這麼寫的話,每次呼叫 line
時,就都會去作一次 Line::Bot::Client.new
。我們可以把它保存起來,第二次呼叫 line
的時候就把保存起來的部分拿出來用,這樣作可以增加效能。
# Line Bot API 物件初始化
def line
return @line unless @line.nil?
@line = Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
如果 @line
有值的話,直接回傳 @line
,沒有值的話才作 Line::Bot::Client.new
並保存到 @line
。
這裡用到了 @
,@
開頭的變數是實體變數,跟區域變數不同的是,實體變數的記憶比較持久,區域變數只要函數執行完就消失,但實體變數可以持續存活到第二次之後的函數執行,甚至我可以在A函數保存實體變數,在B函數去使用實體變數。
關於實體變數,詳細的教學請參考:為你自己學 Ruby on Rails - 變數、常數、流程控制、迴圈
現在的程式已經足夠完美了,但有更精簡的寫法。
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
||
是或的意思,這是一個很特殊的寫法,跟原本的程式碼效果幾乎相同,我就不多作解釋。沒學會 ||=
也沒關係,這就是工程師der浪漫。
目前的完整程式碼如下:
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: '移出 client'
}
# 傳送訊息
response = line.reply_message(reply_token, message)
# 回應 200
head :ok
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
改到這邊可以上傳程式碼測試一下,如果你不測也沒關係,因為我們還要繼續改。
我們作一個函數,讓他自己去抓 reply token,我們關心的是要發什麼話,不關心 reply_token,所以我希望我們的主程式能變成這樣。
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
所以我們要實作函數 reply_to_line
,他是一個傳入 message 後,透過 line api 傳送訊息出去,並傳回 HTTP response 的函數。
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
response = line.reply_message(reply_token, message)
end
可以再精簡為:
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
因為一個函數的傳回值是最後一行的執行結果。這就是工程師der浪漫。
其實我們大部分時間在作的事情都是搬移程式,其實寫程式就是一種整理的藝術。
你可以想像成我們東西一開始全都放在客廳。東西越來越多之後,客廳就會開始變亂。要整理客廳的方法就是買幾個櫃子後把東西放進櫃子。函數就是我們的櫃子。
目前的完整程式碼如下:
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(message)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
這是現在的主程式:
def webhook
# 設定回覆訊息
message = {
type: 'text',
text: '移出 reply_token'
}
# 傳送訊息
response = reply_to_line(message)
# 回應 200
head :ok
end
我不希望在主程式看到這些:
{
type: 'text',
text: 'ㄅㄌㄅㄌㄅㄌ'
}
因為我們只在乎 'ㄅㄌㄅㄌㄅㄌ'
的部分,所以剩餘的部分都要盡量外移。這就像你不會想把垃圾放在桌上一樣,找個垃圾桶放垃圾就對了。
設定目標:
def webhook
# 設定回覆訊息
reply_text = '移出 message'
# 傳送訊息
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
這就是我們所希望的樣子,因此我們要修改 reply_to_line
這個函數。
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
其實就是把一開始的程式整個全搬到 reply_to_line
。
目前的完整程式碼如下:
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = '移出 message'
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 傳送訊息到 line
def reply_to_line(reply_text)
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
...下略
end
現在的主程式已經比一開始乾淨許多,這時我們再來加功能,應該就會比較容易看出我們在加什麼功能。我們要加的功能是關鍵字回覆。
我們希望主程式可以變成這樣:
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
所以我們需要兩個函數,一個是 received_text
:傳回對方說的話,另一個函數是 keyword_reply
:傳入對方說的話,傳回卡米狗應該說的話。
這種思考邏輯是先決定程式的大架構,再來描述細節。這就像在畫圖的時候,你會先打個草稿,草稿看起來OK了再去畫細節,這樣可以確保你不會太專注於細節而失去了整體比例。
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
現在這樣就表示你說什麼,卡米狗就會跟著說什麼。
目前完整的程式碼如下:
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
params['events'][0]['message']['text']
end
# 關鍵字回覆
def keyword_reply(received_text)
received_text
end
...下略
end
這程式碼可以運作,你可以上傳程式碼玩玩看。
這兩個函數現在這樣都還算未完成,他們都有一些缺陷。
先講關鍵字回覆。這裡應該要作成看見A回答B,而不是看見A回答A,我們還缺一個學習紀錄表讓卡米狗查。
而 received_text
的問題是,如果 Line 傳來的通知並不是訊息通知,而是比方說有人加你好友,或邀請你進入群組,或傳送貼圖、圖片、聲音、檔案都可能會導致我們的程式直接掛掉,因為 params['events'][0]['message']['text']
的值是空值,而我們沒有說當是空值的時候應該怎麼作。
根據 Line Messaging API 的文件,當有人傳訊息來時,我們才會收到 message
這個 hash,也就是說,下面這行不一定有值。
params['events'][0]['message']
我們應該在他有值的時候才去取 ['text']
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
nil
else
message['text']
end
end
這是我們第一次用到了 if ,當 message.nil?
是空值的時候,我們就會傳回 nil
。不是空值的話,就傳回 message['text']
。
加入一點工程師的浪漫就會變成這樣:
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
有的時候不用太堅持什麼浪漫,因為這純粹只是工程師的自爽行為。
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
在這裡定義了學習紀錄表 keyword_mapping
之後就作查表。當查表查不到內容的時候,就會傳回 nil。
這表示當查不到內容時,卡米狗應該要不回應,這需要修改其他函數。
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
這裡加一行 return nil if reply_text.nil?
,當傳入值為空時表示不回應,後面的程式碼就不用作了。
目前完整的程式碼如下:
require 'line/bot'
class KamigoController < ApplicationController
protect_from_forgery with: :null_session
def webhook
# 設定回覆文字
reply_text = keyword_reply(received_text)
# 傳送訊息到 line
response = reply_to_line(reply_text)
# 回應 200
head :ok
end
# 取得對方說的話
def received_text
message = params['events'][0]['message']
message['text'] unless message.nil?
end
# 關鍵字回覆
def keyword_reply(received_text)
# 學習紀錄表
keyword_mapping = {
'QQ' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s',
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
# 傳送訊息到 line
def reply_to_line(reply_text)
return nil if reply_text.nil?
# 取得 reply token
reply_token = params['events'][0]['replyToken']
# 設定回覆訊息
message = {
type: 'text',
text: reply_text
}
# 傳送訊息
line.reply_message(reply_token, message)
end
# Line Bot API 物件初始化
def line
@line ||= Line::Bot::Client.new { |config|
config.channel_secret = '9160ce4f0be51cc72c3c8a14119f567a'
config.channel_token = '2ncMtCFECjdTVmopb/QSD1PhqM6ECR4xEqC9uwIzELIsQb+I4wa/s3pZ4BH8hCWeqfkpVGVig/mIPDsMjVcyVbN/WNeTTw5eHEA7hFhaxPmQSY2Cud51LKPPiXY+nUi+QrXy0d7Hi2YUs65B/tVOpgdB04t89/1O/w1cDnyilFU='
}
end
def eat
render plain: "吃土啦"
end
def request_headers
render plain: request.headers.to_h.reject{ |key, value|
key.include? '.'
}.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def response_headers
response.headers['5566'] = 'QQ'
render plain: response.headers.to_h.map{ |key, value|
"#{key}: #{value}"
}.sort.join("\n")
end
def request_body
render plain: request.body
end
def show_response_body
puts "===這是設定前的response.body:#{response.body}==="
render plain: "虎哇花哈哈哈"
puts "===這是設定後的response.body:#{response.body}==="
end
def sent_request
uri = URI('http://localhost:3000/kamigo/eat')
http = Net::HTTP.new(uri.host, uri.port)
http_request = Net::HTTP::Get.new(uri)
http_response = http.request(http_request)
render plain: JSON.pretty_generate({
request_class: request.class,
response_class: response.class,
http_request_class: http_request.class,
http_response_class: http_response.class
})
end
def translate_to_korean(message)
"#{message}油~"
end
end
用起來的效果是這樣:
那個「讓我想想...」是我們在第三天:作一隻最簡單的 Line 聊天機器人從 Line 後台作的設定,如果你不喜歡可以去後台把它關掉,它在這裡:
今天就講到這,明天講怎麼教卡米狗說話。
卡卡米大大你好
我最近都有在看你的系列文 剛剛都進行的很順利 但直到最後面的程式碼部屬crash了
不知道是哪邊出了問題 還請多指教 感謝
他說你要在 Gemfile 裡面加 gem 'pg'
這是detabase的
今天一直想說會不會是因為昨天只打bundle沒打到install才會這樣 可是還是crash了
上國外論壇爬文 終於找到辦法解決 就是把pg降版
gem 'pg', '~> 0.20'
可是不知道這個版本會不會對後續造成甚麼影響
XD 第23天說要改用 sqlite3 了
還好就是一樣在部屬環境上使用0.20 目前23天弄完部屬是沒錯誤
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
nil
else
message['text']
end
end
這段程式碼如果沒有加入工程師的浪漫 XD
是不是要變成底下呀?
# 取得對方說的話
def received_text
message = params['events'][0]['message']
if message.nil?
return nil
else
message
end
end
在第一次if判定中若為nil的話就直接不做底下並且回傳nil ~
然後message['text']會把回傳訊息設定成nil,超囧
我找錯找好久 QQ,我的都是邏輯錯誤XD
最後一行的 return 可以省略,所以 nil 前面可以不加 return,如果你覺得加了 return 對你來說比較好閱讀,那你就加,也是 OK 的
# 關鍵字回覆
def keyword_replya(received_text)
# 學習紀錄表
i = rand(3)
if i = 1
message = {
type: 'text',
text: "壞掉拉"
}
elsif i = 2
message = {
type: 'text',
text: "不要拉"
}
elsif i = 3
message = {
type: 'text',
text: "哪裡錯R"
}
end
keyword_mapping = {
'QQ' => message,
'我難過' => '神曲支援:https://www.youtube.com/watch?v=T0LfHEwEXXw&feature=youtu.be&t=1m13s'
}
# 查表
keyword_mapping[received_text]
end
請問卡米大大 為什麼這樣子打 QQ 都會沒反應
附上log
卡米大,我照著上面的步驟跑(也執行23天的更改了),結果cmd印出了這些錯誤,請問該怎麼解決QAQ
目前機器人也無法做出回應
這些 WARNING 跟機器人無法回應應該是沒關係,我的也有這些,這些是可以解決的,但是過程很複雜,而且不影響機器人功能,所以我就沒有提到。
重新審過一遍,發現是line secret打錯XDD
謝謝卡米大~~~~ 可以繼續往下啦
good~
請問這樣要怎麼改?
請問你進行了什麼操作之後看到這個錯誤
因為bot沒回我,所以我從新回頭做,結果發現代碼顯示500
原本在21天都正常,到第22天改完程式碼就不行了
當你在 postman 傳遞訊息給你的開發環境時,你傳遞的內容是否跟 line 傳遞給 heroku 的內容一致。
你可以透過 heroku logs -t 來觀察正式環境上的錯誤訊息。
錯誤代碼是143,又不一樣了
看起來你的 heroku 上是正常運作的
但沒給我回應?
但沒給我回應?
剛又回去重做21天,又不行了,他一直說我方法錯誤,我不知道如何解決,求解,謝謝!!!
不小心多傳了一張一樣的,抱歉
錯誤500
heroku logs是正常的
line機器人無法回話
如果你想要在 postman 測試你的 server ,那麼就要傳遞與 line 相同格式的內容給你的 server
能夠說得再詳細一點?鄙人不才 真的很想解決
你在 line 上面打字,然後看一下 heroku log,看 line 傳給你什麼東西。
然後你先試著在 postman 傳遞相同的東西到你的 heroku server
先做到這點
請問卡米大,如果我要讓他做
回覆訊息的條件為「只要字句中有包含,就回覆」
EX: 關鍵字:女朋友,回覆'哈哈' 使用者輸入:我沒有女朋友...
LINE_BOT:'哈哈'
類似這樣的指令,我原本想如下
if received_text.(包含..好像不是include) = '關鍵字'
return '哈哈'
可是無法做出,請問有什麼想法或其他的方式嗎?
目前的關鍵字回覆都是 keyword = 'keyword'(100%一樣)
請參考:https://stackoverflow.com/questions/5522274/how-do-i-do-a-like-query-in-activerecord
好的!謝謝卡米大!
感謝卡米大!!後來我是用
return nil unless received_text.include? '(你要的條件)'
來觸發,這樣只要收到的訊息有包含後面的單字即可回復,可是若要再新增其他的條件,好像無法在後面加上
|| received_text.include? '(你要的)'
整串變為
return nil unless received_text.include? 'a' || received_text.include? 'b'
另外疑問, return nil unless received.text.include?'a' 分解是否
unless received.include? 'a'
reply_token = params['events'][0]['replyToken']
message = {
type: 'text',
text: '找我啊,不能嗎QQ'
}
line.reply_message(reply_token, message)
else
nil
end
(我會縮起來,可是發現好像拉長就無法QQ)